//-----------------------------------------------------------------------
// <copyright file="SmtpServerTests.cs" company="aliasvault">
// Copyright (c) aliasvault. All rights reserved.
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------

namespace AliasVault.IntegrationTests.SmtpServer;

using System.Text;
using AliasServerDb;
using AliasVault.Cryptography.Server;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using MimeKit;

/// <summary>
/// SmtpServerTests class.
/// </summary>
[TestFixture]
public class SmtpServerTests
{
    /// <summary>
    /// Example public key for RSA encryption tests. This is a public key generated by the JSInterop on the client.
    /// We use this here to also test the server-side decryption implementation, even though this isn't a real-world scenario.
    /// </summary>
    public const string PublicKey = "{\"alg\":\"RSA-OAEP-256\",\"e\":\"AQAB\",\"ext\":true,\"key_ops\":[\"encrypt\"],\"kty\":\"RSA\",\"n\":\"lW8fRfSvLQiK9uZgm_kFjHMY1SedAZlVvZ_8d_d5oqWezQhan8-Y10Qvx0NMe57sQB3ePnShJFNE33w83kgRNkOyxKJ2FOVKtRptd7CgwIt_l9TPjdrB0J0hFn9b1eit2vpQlOdP_Wa8WvW2eVdXYEMWuBU4-aj8vY2qzcmBc-HhJX-Me9oXhUscJxeqMP4_sNiN7D4I0enrmYicB3JQMhUIwMmNt-0srHTdSvHh_6vFZMqB9ohfh2D9Q0BzYcI8rGEy1RTYsmF1zYyoOOzeRGOcKCVNeLO9LZxfAdm1Eq5zv47uw543cxCZXIZPlXOVriMEtTRwaGzE_3RZmpGJqw\"}";

    /// <summary>
    /// Example private key for RSA encryption tests. This is a private key generated by the JSInterop on the client.
    /// We use this here to also test the server-side decryption implementation, even though this isn't a real-world scenario.
    /// </summary>
    public const string PrivateKey = "{\"alg\":\"RSA-OAEP-256\",\"d\":\"KLByToUaseNym1oNkkrTRPQOHfREXywWWaTXhP8AwtXgEKomqv9G-c6aR-K-T6btY2P-oPj268I0rbnRhSEQdrsmUT5_cp8goYGJrx6MFwGlA32x6klXnus6GDsjkXJi7I5eJL17XV99CDOBtTagFxkNdaBpvClUcHTDvncQ5bGAIrNqS7KADoi-E19BxiW_GcSJiVT4H8kDHCkcgTjZx4rKJjTPqqJOLg_poDrvnTJbsjcXP80kQ1AAENRAvDGhSWzP0IYtP1DM_2FzM1s1b_SrUsS3KiO8drR2Kv-PSOvncpaNVnZGElGCraJ3B2Mm-dr3vFjkyWeWPceqyhtYoQ\",\"dp\":\"ttxRg6uB2YLWfkPKUkzAaBWniZDHM4silJX3IgexA5GJBd9GIhUiVEolc_MgmieQbZ10CC65wqcHVv82lgCeqxYHxHWLxxJCrOpvkFlYE8wr_WqOPQEzYKv3KsL6s6Fj7Pbv9WehWpXdlbJUm4Cy5cgUkdH6PXiwBSvfhCQGrYk\",\"dq\":\"YFqlDAVTfvTR2bMJulvWzd_at81CsEmR-lPo91h-3cLpxcLDOlrTP-d3Ass2I4r1PtBT1bKuuHeQ6fZmHH55a6m8XxPEs2BuIxlh9RiFfWbd66969UOnItuawf0rfGneKt1zl4st60T3KXd8-ECrLxdsvOYpOEuNzvIY_b3qitE\",\"e\":\"AQAB\",\"ext\":true,\"key_ops\":[\"decrypt\"],\"kty\":\"RSA\",\"n\":\"lW8fRfSvLQiK9uZgm_kFjHMY1SedAZlVvZ_8d_d5oqWezQhan8-Y10Qvx0NMe57sQB3ePnShJFNE33w83kgRNkOyxKJ2FOVKtRptd7CgwIt_l9TPjdrB0J0hFn9b1eit2vpQlOdP_Wa8WvW2eVdXYEMWuBU4-aj8vY2qzcmBc-HhJX-Me9oXhUscJxeqMP4_sNiN7D4I0enrmYicB3JQMhUIwMmNt-0srHTdSvHh_6vFZMqB9ohfh2D9Q0BzYcI8rGEy1RTYsmF1zYyoOOzeRGOcKCVNeLO9LZxfAdm1Eq5zv47uw543cxCZXIZPlXOVriMEtTRwaGzE_3RZmpGJqw\",\"p\":\"yUdbuDwmVwKhou5xXUxJfi1eOjN-5F88wtyR4LpgU2OvZ7m-er4hpXx5I2E-KTVX_iIp0Q9VDXhHH-WkN3qg20RXjRoxwgrggYbfdIYdrB-2kbMamq5cOf2XbXGEO8PoDXYoZprIB0EhrD4qVVykPUYg5El0hIKPdfs9LNoOEzs\",\"q\":\"vg93lGTurG0EY179tPr6Qe3ttKEN9zvQ97dZ9034DOWDoWLe-iMKG1-yKmkG4uwC8QqNnm1mPz7EqOuHPPGVTTib9NA4JdM27PUHSPKDUvp0cV4LhF6e-W7tMFk8WbJ2ACqkqhZHYgm-FDkZBCpnehNegTxipLluKa79G__ZHFE\",\"qi\":\"fnI3Wh5aYuxI0R18NTeFKjo1P7_Ck65Gc9O3CmeqiIe58EJaXQEcdwdSOG8aVmn03szXLHEnp7anNIH63f0ericbRYdCQVhcQpvsXzEM_sp4aYmwz45palrjlY4Jc6G6XQn3FwiqqRDvpnXdsunnQ62HHhxmslaEMYHQyLng2ss\"}";

    /// <summary>
    /// The test host instance.
    /// </summary>
    private IHost _testHost;

    /// <summary>
    /// The test host builder instance.
    /// </summary>
    private TestHostBuilder _testHostBuilder;

    /// <summary>
    /// Setup logic for every test.
    /// </summary>
    /// <returns>Task.</returns>
    [SetUp]
    public async Task Setup()
    {
        _testHostBuilder = new TestHostBuilder();
        _testHost = _testHostBuilder.Build();

        await _testHost.StartAsync();

        // Create an AliasVault user, public key and an email claim.
        var dbContext = _testHostBuilder.GetDbContext();
        var user = new AliasVaultUser
        {
            UserName = "testuser",
            Email = "testuser@example.tld",
        };
        dbContext.AliasVaultUsers.Add(user);
        await dbContext.SaveChangesAsync();

        // Create email claims.
        var emailClaim = new UserEmailClaim
        {
            UserId = user.Id,
            Address = "claimed@example.tld",
            AddressLocal = "claimed",
            AddressDomain = "example.tld",
        };
        dbContext.UserEmailClaims.Add(emailClaim);

        var emailClaim2 = new UserEmailClaim
        {
            UserId = user.Id,
            Address = "claimed.cc@example.tld",
            AddressLocal = "claimed.cc",
            AddressDomain = "example.tld",
        };
        dbContext.UserEmailClaims.Add(emailClaim2);

        // Create disabled email claim.
        var emailClaimDisabled = new UserEmailClaim
        {
            UserId = user.Id,
            Address = "disabled@example.tld",
            AddressLocal = "disabled",
            AddressDomain = "example.tld",
            Disabled = true,
        };
        dbContext.UserEmailClaims.Add(emailClaimDisabled);

        // Create public key.
        var encryptionKey = new UserEncryptionKey
        {
            UserId = user.Id,
            PublicKey = PublicKey,
            IsPrimary = true,
        };
        dbContext.UserEncryptionKeys.Add(encryptionKey);

        await dbContext.SaveChangesAsync();
    }

    /// <summary>
    /// Tear down logic for every test.
    /// </summary>
    /// <returns>Task.</returns>
    [TearDown]
    public async Task TearDown()
    {
        await _testHost.StopAsync();
        _testHost.Dispose();
        await _testHostBuilder.DisposeAsync();
    }

    /// <summary>
    /// Tests sending a single email in plain format to the SMTP server with valid claim to check if it is processed correctly.
    /// </summary>
    /// <returns>Task.</returns>
    [Test]
    public async Task SingleEmailPlain()
    {
        // Email the SMTP server.
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
        message.Subject = "Test Email";
        const string textBody = "This is a test email plain.";
        message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();
        await SendMessageToSmtpServer(message);

        // Check if the email is in the database.
        var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();

        // Test non-encrypted field.
        Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));

        // Decrypt the email and then check all individual fields.
        processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
        Assert.Multiple(() =>
        {
            Assert.That(processedEmail, Is.Not.Null);
            Assert.That(processedEmail.From, Is.EqualTo("\"Test Sender\" <sender@example.com>"));
            Assert.That(processedEmail.FromLocal, Is.EqualTo("sender"));
            Assert.That(processedEmail.FromDomain, Is.EqualTo("example.com"));
            Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email plain."));
            Assert.That(processedEmail.MessagePlain, Is.EqualTo("This is a test email plain."));
            Assert.That(processedEmail.MessageHtml, Is.Null);
        });
    }

    /// <summary>
    /// Tests sending a single email in html format to the SMTP server to check if it is processed correctly.
    /// </summary>
    /// <returns>Task.</returns>
    [Test]
    public async Task SingleEmailHtml()
    {
        // Arrange
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
        message.Subject = "Test Email with HTML body.";
        const string htmlBody = "<html><body><h1>This is a test email html.</h1></body></html>";
        message.Body = new BodyBuilder { HtmlBody = htmlBody }.ToMessageBody();
        await SendMessageToSmtpServer(message);

        // Check if the email is in the database.
        var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();

        // Test non-encrypted field.
        Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));

        // Decrypt the email and then check all individual fields.
        processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
        Assert.Multiple(() =>
        {
            Assert.That(processedEmail, Is.Not.Null);
            Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email html."));
            Assert.That(processedEmail.MessagePlain, Is.Null);
            Assert.That(processedEmail.MessageHtml, Is.EqualTo(htmlBody));
        });
    }

    /// <summary>
    /// Tests sending a single email in multipart format to the SMTP server to check if it is processed correctly.
    /// </summary>
    /// <returns>Task.</returns>
    [Test]
    public async Task SingleEmailMultipart()
    {
        // Arrange
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
        message.Subject = "Test Email with multipart body.";
        const string textBody = "This is a test email multipart.";
        const string htmlBody = "<html><body><h1>This is a test email multipart.</h1></body></html>";
        message.Body = new BodyBuilder { TextBody = textBody, HtmlBody = htmlBody }.ToMessageBody();
        await SendMessageToSmtpServer(message);

        // Check if the email is in the database.
        var processedEmail = await _testHostBuilder.GetDbContext().Emails.FirstAsync();

        // Test non-encrypted field.
        Assert.That(processedEmail.To, Is.EqualTo("claimed@example.tld"));

        // Decrypt the email and then check all individual fields.
        processedEmail = EmailEncryption.DecryptEmail(processedEmail, PrivateKey);
        Assert.Multiple(() =>
        {
            Assert.That(processedEmail, Is.Not.Null);
            Assert.That(processedEmail.MessagePreview, Is.EqualTo("This is a test email multipart."));
            Assert.That(processedEmail.MessagePlain, Is.EqualTo("This is a test email multipart."));
            Assert.That(processedEmail.MessageHtml, Is.EqualTo(htmlBody));
        });
    }

    /// <summary>
    /// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
    /// </summary>
    /// <returns>Task.</returns>
    [Test]
    public async Task MultipleRecipientsEmail()
    {
        // Send email to the SMTP server.
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
        message.Cc.Add(new MailboxAddress("Test Recipient 2", "claimed.cc@example.tld"));
        message.Cc.Add(new MailboxAddress("Test Recipient 3 unknown domain", "recipient@unknowndomain.tld"));

        message.Subject = "Test Email";
        const string textBody = "This is a test email plain.";
        message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();
        await SendMessageToSmtpServer(message);

        // Check that two emails are in the database, one for each allowed recipient.
        Assert.That(await _testHostBuilder.GetDbContext().Emails.CountAsync(), Is.EqualTo(2));
    }

    /// <summary>
    /// Tests sending an email to an unknown recipient domain, we expect to get an error from the SMTP server.
    /// </summary>
    [Test]
    public void SingleEmailUnknownRecipientDomain()
    {
        // Send email to the SMTP server.
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "recipient@unknowndomain.tld"));
        message.Subject = "Test Email";
        const string textBody = "This is a test email plain.";
        message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();

        // Expect error from SmtpClient when sending email to unknown domain.
        Assert.ThrowsAsync<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
    }

    /// <summary>
    /// Tests sending an email to an existing but disabled email claim, we expect to get an error from the SMTP server.
    /// </summary>
    [Test]
    public void SingleEmailDisabledUserClaim()
    {
        // Send email to the SMTP server.
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "disabled@example.tld"));
        message.Subject = "Test Email";
        const string textBody = "This is a test email plain.";
        message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();

        // Expect error from SmtpClient when sending email to unknown domain.
        Assert.ThrowsAsync<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
    }

    /// <summary>
    /// Tests sending a single email to a known recipient domain but with no valid user claim. We expect
    /// to get an error from the SMTP server.
    /// </summary>
    [Test]
    public void SingleEmailNoUserClaim()
    {
        // Send email to the SMTP server.
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "not-claimed@example.tld"));
        message.Subject = "Test Email";
        const string textBody = "This is a test email plain.";
        message.Body = new BodyBuilder { TextBody = textBody }.ToMessageBody();

        // Expect error from SmtpClient when sending email to unknown domain.
        Assert.ThrowsAsync<SmtpCommandException>(async () => await SendMessageToSmtpServer(message));
    }

    /// <summary>
    /// Tests sending a single email in plain format to the SMTP server to check if it is processed correctly.
    /// </summary>
    /// <returns>Task.</returns>
    [Test]
    public async Task AttachmentEmail()
    {
        var message = new MimeMessage();
        message.From.Add(new MailboxAddress("Test Sender", "sender@example.com"));
        message.To.Add(new MailboxAddress("Test Recipient", "claimed@example.tld"));
        message.Subject = "Test Email with attachment";

        var bodyBuilder = new BodyBuilder();
        bodyBuilder.TextBody = "This is a test email with attachment.";

        // Add attachment using BodyBuilder
        byte[] attachmentData = Encoding.UTF8.GetBytes("This is an attachment.");
        bodyBuilder.Attachments.Add("attachment.txt", attachmentData, ContentType.Parse("text/plain"));

        message.Body = bodyBuilder.ToMessageBody();

        await SendMessageToSmtpServer(message);

        // Check that attachment is in the database and the bytes are encrypted.
        Assert.That(await _testHostBuilder.GetDbContext().EmailAttachments.CountAsync(), Is.EqualTo(1));
        var attachment = await _testHostBuilder.GetDbContext().EmailAttachments.FirstAsync();
        Assert.That(attachment.Bytes, Is.Not.EqualTo(attachmentData), "Email attachment bytes are not encrypted. Check email encryption logic.");
    }

    /// <summary>
    /// Sends a message to the SMTP server.
    /// </summary>
    /// <param name="message">MimeMessage to send.</param>
    private static async Task SendMessageToSmtpServer(MimeMessage message)
    {
        using var client = new SmtpClient();

        await client.ConnectAsync("localhost", 2525, SecureSocketOptions.None);
        try
        {
            await client.SendAsync(message);
        }
        finally
        {
            await client.DisconnectAsync(true);
        }
    }
}
